/*
 * File     : CallbackManager.java
 * Created  : 5 Jan 2012
 *
 * Copyright © 2011 Matthew Wilson (mj. {my-surname} .uk {at} gmail.com)
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Lesser General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Lesser General Public License for more details.
 *
 * You should have received a copy of the GNU Lesser General Public License
 * along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package com.googlecode.dni.internal.callback;

import static com.googlecode.dni.internal.DniInternal.PLATFORM;

import java.lang.reflect.Method;
import java.nio.ByteBuffer;
import java.util.Arrays;
import java.util.BitSet;
import java.util.EnumMap;
import java.util.Map;

import com.googlecode.dni.callback.FunctionPointer;
import com.googlecode.dni.internal.DniInternal;
import com.googlecode.dni.library.CallingConvention;
import com.googlecode.dni.type.Pointer;

/**
 * <p>
 *  Manages creation of thunks that call from native code into Java.
 * </p>
 * <p>
 *  The generic pattern is that there is a table of jump instructions.  Each
 *  entry first loads a constant that identifies that entry; the constant will
 *  be a pointer to a structure holding information necessary to make the call
 *  back into Java.  In this manner, 95% of the thunk code is the same and
 *  statically defined; there is no need to create copies, nor to use code
 *  modification.
 * </p>
 * <p>
 *  The actual instructions and format are controlled by the calling convention.
 *  This is abstracted out to the {@link ThunkManagerDelegate} interface.
 * </p>
 *
 * @author Matthew Wilson
 */
public final class ThunkManager
{

    private static final Map< CallingConvention, ThunkManager > INSTANCES =
        new EnumMap< CallingConvention, ThunkManager >( CallingConvention.class );

    static
    {
        for ( CallingConvention callingConvention : CallingConvention.values() )
        {
            if ( callingConvention.isSupported() )
            {
                ThunkManager manager = INSTANCES.get( callingConvention.getActual() );
                if ( manager == null )
                {
                    manager = new ThunkManager( DniInternal.PLATFORM.getThunkManagerDelegate( callingConvention ) );
                }
                INSTANCES.put( callingConvention, manager );
            }
        }
    }


    private final ThunkManagerDelegate thunkManagerDelegate;

    private final int jumpEntrySize;

    private final int infoEntrySize;

    private Chunk[] chunks;


    /**
     * @param callingConvention
     *            the calling convention
     * @return the instance
     */
    public static ThunkManager getInstance( final CallingConvention callingConvention )
    {
        ThunkManager manager = INSTANCES.get( callingConvention );
        if ( manager == null )
        {
            throw new UnsupportedOperationException( callingConvention.toString() );
        }
        return manager;
    }

    /**
     * @param thunkManagerDelegate
     *            the platform-specific delegate
     */
    ThunkManager( final ThunkManagerDelegate thunkManagerDelegate )
    {
        this.thunkManagerDelegate = thunkManagerDelegate;
        this.jumpEntrySize = thunkManagerDelegate.getJumpEntrySize();
        this.infoEntrySize = thunkManagerDelegate.getInfoEntrySize();

        assert Integer.bitCount( this.jumpEntrySize ) == 1;
        assert Integer.bitCount( this.infoEntrySize ) == 1;
    }

    /**
     * Wraps a delegate as a function pointer.
     *
     * @param <T>
     *            the Java call-back type
     * @param record
     *            the call-back record
     * @param delegate
     *            the delegate
     * @return the function pointer
     */
    public < T > FunctionPointer< T > wrap( final CallbackRecord< T > record,
                                            final T delegate )
    {
        if ( !record.getType().isInstance( delegate ) )
        {
            throw new IllegalArgumentException();
        }

        // if it's already a native function, no need to wrap it in more code
        // TODO v1.0 what about hierarchies of callbacks interfaces? need to be more careful here
//        if ( delegate instanceof AbstractCallback )
//        {
//            return new NativeFunctionPointer( ( (AbstractCallback) delegate ).pointer,
//                                              record.getCallingContention(),
//                                              delegate );
//        }

        synchronized ( this )
        {
            // work through the chunks until we find one that has space
            long pointer = 0;
            if ( this.chunks == null )
            {
                this.chunks = new Chunk[] { new Chunk( 1 ) };
            }

            for ( Chunk chunk : this.chunks )
            {
                pointer = chunk.allocate( record, delegate );
                if ( pointer != 0 )
                {
                    break;
                }
            }

            // none had space; allocate a new one
            if ( pointer == 0L )
            {
                Chunk chunk = new Chunk( this.chunks.length + 1 );

                pointer = chunk.allocate( record, delegate );
                if ( pointer == 0 )
                {
                    throw new AssertionError();
                }

                this.chunks = Arrays.copyOf( this.chunks, this.chunks.length + 1 );
                this.chunks[ this.chunks.length - 1 ] = chunk;
            }

            DniInternal.LOGGER.fine( "Allocated thunk " + Long.toHexString( pointer ) + " for " + delegate );
            return new JavaFunctionPointer< T >( pointer, delegate, record.getCallingContention() );
        }
    }

    /**
     * Frees a native thunk allocated for the given delegate.
     *
     * @param pointer
     *            the function pointer
     * @param delegate
     *            the delegate
     */
    public void free( final long pointer,
                      final Object delegate )
    {
        // TODO v1.0 Actually, have one impl of FunctionPointer.
        // Freeing a NativeFunctionPointer will try to look for the function,
        // and if it's not there, ignore it (or log it or complain).
        // That way, there is no extra state hanging around in a
        // JavaFunctionPointer, allowing it to be stored in a structure (yay).

        synchronized ( this )
        {
            // work through the chunks until we find the one that allocated it
            if ( this.chunks != null )
            {
                for ( Chunk chunk : this.chunks )
                {
                    if ( chunk.free( pointer, delegate ) )
                    {
                        DniInternal.LOGGER.fine( "Freed thunk " + Long.toHexString( pointer ) );
                        return;
                    }
                }
            }

            throw new AssertionError( "Failed to free: 0x" + Long.toHexString( pointer ) + " for " + delegate );
        }
    }

    /**
     * Holds two (sets of) pages, one containing jump instructions, and the
     * other containing info entries.  Each jump entry uniquely identifies an
     * info entry, thereby allowing a generic thunk to call a different Java
     * method with different callback object.
     */
    private final class Chunk
    {
        private final long jumpTable;

        private final long infoTable;

        /** Bit set marking which entries have been used. */
        private final BitSet used;

        /** Size of the jump table in bytes. */
        private final int jumpTableSize;

        /** Size of the info table in bytes. */
        private final int infoTableSize;

        /** Number of entries. */
        private final int count;


        Chunk( final int chunkCount )
        {
            DniInternal.LOGGER.info( "Allocating chunk of thunks" );

            int pageSize = PLATFORM.getPageSize();

            // later chunks can be bigger
            int baseSize = pageSize * Math.min( 16, chunkCount );
            assert Integer.bitCount( baseSize ) == 1 : baseSize;

            // determine how big the jump and info entry tables are to be
            if ( ThunkManager.this.jumpEntrySize >= ThunkManager.this.infoEntrySize )
            {
                this.jumpTableSize = baseSize / ThunkManager.this.infoEntrySize * ThunkManager.this.jumpEntrySize;
                this.infoTableSize = baseSize;
            }
            else
            {
                this.jumpTableSize = baseSize;
                this.infoTableSize = baseSize / ThunkManager.this.jumpEntrySize * ThunkManager.this.infoEntrySize;
            }

            // allocate them
            this.jumpTable = PLATFORM.allocatePages( this.jumpTableSize, false, true ).address();
            this.infoTable = PLATFORM.allocatePages( this.infoTableSize, false, true ).address();

            this.count = this.jumpTableSize / ThunkManager.this.jumpEntrySize;
            this.used = new BitSet( this.count );

            assert ( this.count == this.infoTableSize / ThunkManager.this.infoEntrySize )
                   && ( this.jumpTableSize % ThunkManager.this.jumpEntrySize == 0 )
                   && ( this.infoTableSize % ThunkManager.this.infoEntrySize == 0 )
                   : Arrays.toString( new int[] { this.jumpTableSize, this.infoTableSize, ThunkManager.this.jumpEntrySize, ThunkManager.this.infoEntrySize, this.count } );

            writeJumpTable();
        }

        /**
         * Allocates a thunk.
         *
         * @param record
         *            the call-back record
         * @param delegate
         *            the delegate
         * @return the address, or 0 if this chunk is full
         */
        long allocate( final CallbackRecord< ? > record, final Object delegate )
        {
            int index = this.used.nextClearBit( 0 );
            if ( index >= this.count )
            {
                return 0L;
            }

            this.used.set( index );

            // fill in the info tables
            long infoAddress = this.infoTable + ThunkManager.this.infoEntrySize * index;
            ThunkManager.this.thunkManagerDelegate.fillEntry( infoAddress, record.getImplType(), record.getHelperMethod(), delegate );

            // calculate the jump address
            long jumpAddress = this.jumpTable + ThunkManager.this.jumpEntrySize * index;
            return jumpAddress;
        }

        /**
         * Frees a thunk.
         *
         * @param jumpAddress
         *            the address of the jump entry
         * @param delegate
         *            the delegate
         * @return <code>true</code> if successful;
         *         <code>false</code> if the thunk does not belong in this chunk
         */
        boolean free( final long jumpAddress,
                      final Object delegate )
        {
            if ( jumpAddress < this.jumpTable || jumpAddress >= this.jumpTable + this.count * ThunkManager.this.jumpEntrySize )
            {
                return false;
            }

            int index = (int) ( ( jumpAddress - this.jumpTable ) / ThunkManager.this.jumpEntrySize );
            long infoAddress = this.infoTable + ThunkManager.this.infoEntrySize * index;

            // do some basic checks here
            assert checkFree( jumpAddress, index );

            // clear in the tables
            ThunkManager.this.thunkManagerDelegate.clearEntry( infoAddress, delegate );

            this.used.clear( index );

            return true;
        }

        private void writeJumpTable()
        {
            // write the jump table now so that the page can be marked read-only
            ByteBuffer jump = Pointer.fromAddress( this.jumpTable ).getBuffer( this.jumpTableSize );
            for ( int i = 0; i < this.count; i++ )
            {
                jump.limit( ( i + 1 ) * ThunkManager.this.jumpEntrySize );
                jump.position( i * ThunkManager.this.jumpEntrySize );

                ThunkManager.this.thunkManagerDelegate.writeJumpEntry( jump, this.infoTable + i * ThunkManager.this.infoEntrySize );
            }

            PLATFORM.protectPages( Pointer.fromAddress( this.jumpTable ), this.jumpTableSize, true, true );

            DniInternal.MEMORY_ACCESSOR.zeroMemory( this.infoTable, this.infoTableSize );

            DniInternal.LOGGER.fine( "Allocated chunk of " + this.count + " thunks; "
                                     + "jump = 0x" + Long.toHexString( this.jumpTable ) + "; "
                                     + "info = 0x" + Long.toHexString( this.infoTable ) );
        }

        private boolean checkFree( final long jumpAddress, final int index)
        {
            long offset = jumpAddress - this.jumpTable;

            assert offset % ThunkManager.this.jumpEntrySize == 0 : "Incorrect callback freed: " + this + " " + Long.toHexString( jumpAddress );
            assert this.used.get( index ) : "Double-free of callback: " + this + " " + Long.toHexString( jumpAddress );

            return true;
        }
    }


    /**
     * Handles calling convention- and platform-specific details of
     * {@link ThunkManager}.
     *
     * @author Matthew Wilson
     */
    public interface ThunkManagerDelegate
    {
        /**
         * @return size in bytes of the jump entry; must be a power of two
         */
        int getJumpEntrySize();

        /**
         * @return size in bytes of the info entry; must be a power of two
         */
        int getInfoEntrySize();

        /**
         * Writes a jump table entry into the given buffer, starting at the
         * current position of the buffer.
         *
         * @param jumpBuffer
         *            the destination buffer
         * @param infoAddress
         *            the address of the info entry
         */
        void writeJumpEntry( ByteBuffer jumpBuffer, long infoAddress );

        /**
         * Fills an info entry with the given values.
         *
         * @param infoAddress
         *            the address of the info entry
         * @param implType
         *            the implementation type, holding the given method
         * @param helperMethod
         *            the helper method
         * @param delegate
         *            the delegate
         */
        void fillEntry( long infoAddress,
                        Class< ? > implType,
                        Method helperMethod,
                        Object delegate );

        /**
         * Clears an info entry, checking the given values.
         *
         * @param infoAddress
         *            the address of the info entry
         * @param delegate
         *            the delegate
         */
        void clearEntry( long infoAddress,
                         Object delegate );

    }

}
